记录一次非典型迁移:从 OpenResty 到 Dokploy 的摩擦
记录一次非典型迁移:从 OpenResty 到 Dokploy 的踩坑
服务器原本运行在一种相对稳态中:1Panel 管理系统进程,OpenResty 处理所有流量入口,SSL 证书通过 acme.sh 自动续期。
打破这种稳态的动因是对 Dokploy 的尝试欲。我试图在一个已经跑满业务的单机环境上,引入这套基于 Docker Swarm 的 PaaS 方案。
预想中的“平滑过渡”并未发生。随后的过程与其说是技术升级,不如说是对端口独占权、SSL 终止点以及容器网络认知的一次强制校准。
一、端口冲突与独占权的让渡
Dokploy 的安装逻辑具有很强的侵入性。脚本预设它必须拥有对流量入口的绝对控制权:
80:HTTP 入口及 ACME 验证443:HTTPS 入口3000:面板访问
安装脚本的检查逻辑简单粗暴:一旦检测到端口占用,直接终止。
我的服务器现状是:OpenResty 正好占据了 80 和 443。
第一次尝试:让位
我试图将 OpenResty 迁移至 8080/8443,以此腾出主入口。这是第一次认知修正:仅仅修改主配置文件是不够的。
在修改了 /opt/1panel/.../nginx.conf 后,ss -tulnp 依然显示 80 端口被占用。排查后发现,包含在 conf.d 目录下的 00.default.conf 同样硬编码了监听端口。
这暴露了我对 1Panel 管理下的 OpenResty 配置结构不够熟悉。彻底清理所有 listen 80 配置并重启服务后,Traefik(Dokploy 的网关)才得以启动。
此时架构变成了:
公网 -> Traefik (80/443) -> ?
旧服务全部下线,等待接驳。
二、嵌套代理的失败试验
为了快速恢复旧站点,我构想了一个过渡方案:让 Traefik 将旧域名的流量转发给运行在 8080 的 OpenResty。
配置看似合理:
- Traefik 识别 Host。
- 转发至
http://127.0.0.1:8080。
然而,浏览器返回了 NET::ERR_CERT_AUTHORITY_INVALID。
原因复盘:
这里出现了双重 SSL 与 HSTS 的冲突。
- 旧站点通过 OpenResty 配置了 Let's Encrypt 证书,并启用了 HSTS(强制 HTTPS)。
- OpenResty 在
8443上依然配置了ssl_certificate。 - Traefik 作为新网关,尝试建立自己的 TLS 连接(或使用自签证书)。
- 浏览器检测到证书链与缓存的 HSTS 策略不符,直接阻断。
结论: 在一个网络拓扑中,SSL 终止点(Termination Point)应当是唯一的。试图在 Traefik 后面再挂一个处理 SSL 的 Nginx,不仅增加了延迟,更引入了难以维护的证书链管理问题。
我放弃了“套娃”方案,决定彻底拆除 OpenResty 的入口职能,转向全面迁移。
三、构建方式的差异:以 React 项目为例
第一个迁移对象是 React 前端。我选择了 Nixpacks 作为构建工具,意图跳过编写 Dockerfile 的步骤。
问题: 部署后出现 502 Bad Gateway。
排查与修正: 这是对“端口”概念的一次混淆。
- 容器视角: Nixpacks 生成的镜像默认监听
80。 - 编排视角: 我在 Dokploy 配置中习惯性填写了
3000(受开发习惯影响)。 - 结果: Traefik 试图将流量转发给容器的 3000 端口,但该端口并未开放。
修正配置为 80 后服务恢复。这提醒我:环境变量 PORT 是给应用看的,而 Dokploy 的端口配置是给网关路由看的。两者必须显式对齐。
四、宿主机服务的接驳:Socat 方案
对于运行在宿主机 4000 端口的后端服务(NewAPI),我不希望立即容器化。如何在 Docker 容器网络(Traefik)与宿主机网络之间建立桥梁?
直接使用 host.docker.internal 在生产环境并不总是开箱即用,且依赖 Docker 版本。
我采用了一个更原始但可控的方案:Socat 桥接容器。
在 Dokploy 中创建一个服务,仅运行 alpine/socat:
services:
bridge:
image: alpine/socat
command: tcp-listen:80,fork,reuseaddr tcp:host.docker.internal:4000
extra_hosts:
- "host.docker.internal:host-gateway"
逻辑链条:
用户 -> Traefik -> Socat容器(80) -> (通过 host-gateway) -> 宿主机(4000)
在此过程中,我再次混淆了测试视角:
- 在宿主机执行
curl http://host.docker.internal是无效的,因为这个 DNS 仅存在于容器网络内部。 - 必须进入容器内部测试连通性,或直接检查宿主机服务是否监听了
0.0.0.0(而非127.0.0.1)。
五、从路径转发到子域名
最后一个迁移对象是 Todo 应用的后端,原先通过 domain.com/api/ 进行路径转发。
在 Traefik 中复刻路径剥离(StripPrefix)规则增加了配置复杂度,且容易产生路径末尾斜杠(trailing slash)的兼容性问题。
决策调整:
放弃路径反代,改用子域名 api.domain.com。
虽然这需要修改前端代码中的 API Base URL 并重新构建,但从长远看,独立的子域名隔离了路由逻辑,降低了网关层的维护成本。
六、当前状态
经过数小时的折腾,服务器状态如下:
- 入口统一: Dokploy (Traefik) 接管了 80/443。
- 旧网关退场: OpenResty 仅作为遗留服务的临时容器,不再处理 SSL,随时可被替代。
- 服务形态混和:
- 纯静态前端:通过 Buildpack 托管。
- 宿主机后端:通过 Socat 容器桥接。
- 新业务:直接 Docker 化。
认知小结:
- Ingress 必须具备排他性。 除非有非常高级的流量分发需求,否则不要尝试在同一台机器上运行两个反向代理入口。
- 网络边界的意识。 在容器化部署中,必须时刻分清“容器内 IP”、“网桥 IP”和“宿主机 IP”。
localhost的含义随上下文变化,是导致大部分连接拒绝的根源。 - 迁移策略。 相比于通过技术手段兼容旧架构(如双层 SSL),修改业务架构(如改用子域名)往往是更彻底的解决方案。